Object-Oriented Persistence

Converting data into objects and vice versa

Created by Ignasi 'Iggy' Bosch / @ignasibosch

WHO I AM IN 90 seconds

Ignasi Bosch Iggy Girona 100km Barcelona Musician 15y PHP CodeIgniter Symfony Laravel PhalconPHP Silex Slim Lumen Android MEAN MongoDB Express Angular NodeJS Python

  1. Introduction
  2. SQL
  3. ORM
  4. DBAL
  5. Patterns
    • Table Data Gateway
    • Row Data Gateway
    • Active Record
    • Data Mapper
  6. ORM Frameworks
    • What
    • When

What's wrong with this?


SELECT `customerName`, `phone` FROM `customers`;
                

SELECT WEEK(`orderDate`) as `week`,
    `offices`.`officeCode` as `office`,
    `productName`,
    MAX(`priceEach`) as `priceEach`
FROM `orders`
RIGHT JOIN `orderdetails`
    ON `orders`.`orderNumber` = `orderdetails`.`orderNumber`
LEFT JOIN `products`
    ON `orderdetails`.`productCode` = `products`.`productCode`
LEFT JOIN `customers`
    ON `orders`.`customerNumber` = `customers`.`customerNumber`
LEFT JOIN `employees`
    ON `customers`.`employeeNumber` = `employees`.`employeeNumber`
LEFT JOIN `offices`
    ON `employees`.`officeCode` = `offices`.`officeCode`
WHERE YEAR(`orderDate`) = 2015
GROUP BY `week`, `offices`.`officeCode`
ORDER BY `week`, `offices`.`officeCode` ASC
                    

...and here?

... are you sure?

... really???

ORM: Object-Relational Mapping

* ORM != (ORM Framework || ORM Library || ORM Software || ORM Tools)

::Dealing with objects and entities
::Access to parents, siblings and child

$purchase = $customer->getPurchases()->last();

foreach($purchase->getDetail() as $detail){

    $category = $detail->getProduct()->getCategory();

    if($category->hasOffer()){
       yield $category->getOffer()->getRules();
    }
}
                     
::Inheritance and Polymorphism
::Memory managment: proxy objects, lazy load

function getPurchaseProductsName(PurchaseInterface $purchase){

    foreach($purchase->getDetail() as $detail){
       yield $detail->getProduct()->getName();
    }
}

$purchase = $customer->getPurchases()->last();

foreach(getPurchaseProductsName($purchase) as $productName){
   echo $productName; <-------- 
}
                     

SELECT `name` FROM `purchase_details` WHERE ....
                    

ODM: Object Document Mapper

DBAL: Database Abstraction Layer

  • Database decoupling
  • One only point as database interface
  • One API, a lot of drivers
  • Transaction management
  • Cache managment
  • Concurrency control
  • Query optimization
  • Synchronize mapping structure with db and vice versa
  • Filter to prevent SQL injections and data sanitize
  • Database error handling
  • ...

TABLE DATA GATEWAY

CustomersPDOGateway.php

class CustomersPDOGateway implements CustomersGatewayInterface
{
    private $conn;
    
    public function __construct(\PDO $conn){
        $this->conn = $conn;
    }
    
    public function findAll($limit = 100, $offset = 0)
    {
        $prepareQuery = $this->conn->prepare('SELECT * FROM customers LIMIT :limit');
        $limit = $limit ? $limit : 100;
        $limit .= $offset ? ', ' . $offset : '';
        $prepareQuery->bindValue('limit', $limit, \PDO::PARAM_STR);
        $prepareQuery->execute();
        return $prepareQuery->fetchAll(\PDO::FETCH_ASSOC);
    }
    
    public function find($id)
    {
        $prepareQuery = $this->conn->prepare('SELECT * FROM customers WHERE id = :id');
        $prepareQuery->bindValue('id', $id, \PDO::PARAM_INT);
        $prepareQuery->execute();
        return $prepareQuery->fetch(\PDO::FETCH_ASSOC);
    }
    
    public function insert($name, $lastName)
    {
        $prepareQuery = $this->conn
                ->prepare('INSERT INTO customers (name, last_name) VALUES (:name, :last_name)');
        $prepareQuery->bindValue('name', $name, \PDO::PARAM_STR);
        $prepareQuery->bindValue('last_name', $lastName, \PDO::PARAM_STR);
        $result = $prepareQuery->execute();
        return $result ? $conn->lastInsertId() : false;
    }
    
    //...
}
                    

$conn = new PDO("mysql:dbname=$dbname;host=$host;charset=utf8", $username, $password, $options);

$customersTable = new CustomersPDOGateway($conn);

$newCustomerId = $customersTable->insert('John', 'Smith');

$customersTable->delete($newCustomerId);
                    

$customers = $customersTable->findAll();

foreach($customers as $customer){
    yield $customer['name'] . ' ' . $customer['last_name'];
}
                    

$customer = $customersTable->find(15);

if($customer){
    echo $customer['name'] . ' ' . $customer['last_name'];
}
                    

ROW DATA GATEWAY

CustomerPDOGateway.php

class CustomerPDOGateway implements CustomerGatewayInterface
{
    private $id;
    private $name;
    private $lastName;
    
    //.. Getters and Setters
    
    static public load(array $rs){
        $customer = new static();
        $customer->setId($rs['id'])
                 ->setName($rs['name'])
                 ->setName($rs['last_name']);
        return $customer;
    }
    
    public function insert(\PDO $conn)
    {
        $prepareQuery = $conn
                ->prepare('INSERT INTO customers (name, last_name) VALUES (:name, :last_name)');
        $prepareQuery->bindValue('name', $this->name, \PDO::PARAM_STR);
        $prepareQuery->bindValue('last_name', $this->lastName, \PDO::PARAM_STR);
        $prepareQuery->execute();
        $this->id = $conn->lastInsertId();
        return $this;
    }
    //...
}
                    

$conn = new PDO("mysql:dbname=$dbname;host=$host;charset=utf8",  $username, $password, $options);

$customer = new CustomerPDOGateway();
$customer->setName('John');
$customer->setLastName('Smith');

$customer->insert($conn);
                    
CustomerPDOFinder.php

class CustomerPDOFinder implements CustomerFinder
{
    static private $conn;
    
    static public setConnection(\PDO $conn){
        static::$conn = $conn;
    }
    
    static public find($id)
    {
        $prepareQuery = static::$conn->prepare('SELECT * FROM customers WHERE id = :id');
        $prepareQuery->bindValue('id', $id, \PDO::PARAM_INT);
        $prepareQuery->execute();
        $result = $prepareQuery->fetch(\PDO::FETCH_ASSOC);
        return $result ? CustomerPDOGateway::load($result) : false;
    }
}
                    

$conn = new PDO("mysql:dbname=$dbname;host=$host;charset=utf8",  $username, $password, $options);
CustomerPDOFinder::setConnection($conn);

$customer = CustomerPDOFinder::find(15);

if($customer){  
    echo $customer->getName() . ' ' . $customer->getLastName();
}
                    

Active Record


$flights = App\Flight::where('active', 1)
            ->orderBy('name', 'desc')
            ->take(10)
            ->get();
                

$flight = new Flight;
$flight->name = $request->name;
$flight->save();
                

$flight = App\Flight::find(1);
$flight->name = 'New Flight Name';
$flight->save();
                

$flight = App\Flight::find(1);
$flight->delete();
                

class Post extends Model
{
    /**
    * Get the comments for the blog post.
    */
    public function comments()
    {
        return $this->hasMany('App\Comment');
    }
}
                    

class Comment extends Model
{
    /**
    * Get the post that owns the comment.
    */
    public function post()
    {
        return $this->belongsTo('App\Post');
    }
}
            

// Retrieve all posts that have three or more comments...
$posts = Post::has('comments', '>=', 3)->get();

foreach ($posts as $post) {
    //...
}
               

$comments = App\Post::find(1)->comments;

foreach ($comments as $comment) {
    //...
}
                       

$comment = new App\Comment(['message' => 'A new comment.']);

$post = App\Post::find(1);

$post->comments()->save($comment);
                    

DATA MAPPER


$productRepository = $entityManager->getRepository('Product');

$products = $productRepository->findAll();

foreach ($products as $product) {
    echo sprintf("-%s\n", $product->getName());
}
                    

$product = new Product();
$product->setName($newProductName);

$entityManager->persist($product);

$entityManager->flush();

echo "Created Product with ID " . $product->getId() . "\n";
                    

$productRepository = $entityManager->getRepository('Product');

$product = $productRepository->findOneByName('Awesome Product');
$product->setName($newProductName);

$entityManager->flush();

echo "Updated Product with ID " . $product->getId() . "\n";
                    
src/Product.php

class Product
{
    /**
    * @var int
    */
    protected $id;
    /**
    * @var string
    */
    protected $name;
    
    public function getId()
    {
        return $this->id;
    }
    
    public function getName()
    {
        return $this->name;
    }
    
    public function setName($name)
    {
        $this->name = $name;
    }
}
                    
src/Product.php

/**
* @Entity @Table(name="products")
**/
class Product
{
    /** @Id @Column(type="integer") @GeneratedValue **/
    protected $id;
    /** @Column(type="string") **/
    protected $name;
    
    // .. (other code)
}
                    
config/xml/Product.dcm.xml


    
        
            
        

        
    

                    
config/yaml/Product.dcm.yml

Product:
    type: entity
    table: products
    id:
        id:
            type: integer
        generator:
            strategy: AUTO
    fields:
        name:
            type: string
                    

PATTERN + MAPPING STRUCTURE + ORM + DBAL + TOOLS (String manipulation, Reflection, Cache, Annotations, Collections, Hooks, Events, Commands, Debugger, Drivers, Service Providers, ...)

When?

some Cool stuff:

Martin Fowler: Active Record
Martin Fowler: Data Mapper
Dev Discussions - ActiveRecord, Data Mapper, and Doctrine
PHP Object-Relational Mapping Libraries In Action
RubyConf 2015 - Ruby Preserves by Craig Buchek
Beyond The ORM - Piotr Solnica - RuLu 2012
Piotr Szotkowski: Decoupling Persistence (Like There's Some Tomorrow)
Database Design Patterns with PHP 5.3
Welcome to Doctrine 2 ORM’s documentation!
Propel: Active Record Reference
Eloquent: Getting Started

  • Chapter 3. Mapping to Relational Databases
    • Architectural Patterns
    • The Behavioral Problem
    • Reading in Data
    • Structural Mapping Patterns
    • Building the Mapping
    • Using Metadata
    • Database Connections
    • Some Miscellaneous Points
    • Further Reading
  • Chapter 10. Data Source Architectural Patterns
    • Table Data Gateway
    • Row Data Gateway
    • Active Record
    • Data Mapper
  • Chapter 11. Object-Relational Behavioral Patterns
    • Unit of Work
    • Identity Map
    • Lazy Load
  • Chapter 12. Object-Relational Structural Patterns
    • Identity Field
    • Foreign Key Mapping
    • Association Table Mapping
    • Dependent Mapping
    • Embedded Value
    • Serialized LOB
    • Single Table Inheritance
    • Class Table Inheritance
    • Concrete Table Inheritance
    • Inheritance Mappers
  • Chapter 13. Object-Relational Metadata Mapping Patterns
    • Metadata Mapping
    • Query Object
    • Repository

Thank You

https://ignasibosch.com/talks/object-oriented-persistence

me@ignasibosch.com | @ignasibosch